Repository navigation
feat(webui): the workspace panel is a browser tab strip - #216
Conversation
- Added IMPLEMENT-SPEC-PROMPT.md to release/public-source.json
…wser Issue #200. The programme's later tickets are written against four assumptions that no type checker catches. A spike settles all four against a real isolated Chromium, and the capability matrix is written from what it observed. Measured: two pages of one Chromium can be driven, streamed and input into at the same time; a navigation on the second page advances that page's generation (1 -> 2) and leaves the first alone; a reference recorded against a page's previous generation is refused with browser_target_stale while the untouched page still accepts its own; and the frame's page is resolved from the envelope's event.sessionId, not from event.params.sessionId, which is the number 1 for frames from both pages. Two findings the repository has to act on, both already in scope: the provider's screencast handler stamps every frame with its own target id, and the provider's generation fence covers one page. The record is .plan/workspace-browser-tab/complete/two-page-spike.json.
Issue #201. The provider drove exactly one page and refused every other target, so a tab that was not the launch page could not be driven, streamed or input into at all. The single page becomes a per-target map: target id -> session, generation, frame sequence and frame handler. The session is resolved by target in sendCommand, sendInput, startImageStream and stopImageStream; the generation check is per target rather than per browser; and a screencast frame is matched to its page by the envelope's event.sessionId, which is the flattened target session the CDP connection puts on the event. event.params.sessionId is the numeric screencast acknowledgement id and names no page -- two pages' frames carry the same one. The generation moves with the map. Page.enable goes to every page, so a page that is not the launch page receives the navigation events that raise its generation, and the raise is keyed by the event's own session, so one page navigating leaves every other page's generation where it was. The provider's own comment about the single page is replaced by the rule that replaces it, and the rule names why the generation is per page. open_tab now attaches to the page it creates and registers it, so the provider really does hold more than one page; a page the provider never attached to is still refused rather than driven. One browser fact the implementation had to meet: Chrome renders the foregrounded tab and throttles the others, so a background page's screencast produces no new frames. The provider therefore brings the page it is streaming to the front, the same thing a browser does when a person switches to a tab. The new suite drives two real pages of a real isolated Chromium and proves that each is driven, streamed and input into independently, that a navigation advances one page's generation and leaves the other's alone, that a stale reference is refused on the navigated page while the untouched page still accepts its own, and that a frame reaches the page that asked for it and no other. The capability matrix is updated: the provider's rows are implemented, and the foreground rule is recorded as measured.
Issue #202. The action created a page and returned the browser's own answer, so the page it created could be named by nobody: the service learned about it only when the provider happened to be asked for its list again. The action now answers with the page's identity -- the surface, the browser id, the target id and the generation the provider's own accounting gives it -- and the service observes that target in the registry from the answer alone. The identity is therefore usable at once: it can be selected and input into without waiting for the next listing, which is what "the service observes the new target from that identity alone" has to mean in practice. The bridge is unchanged, and that is now a tested fact rather than an assumption: open_tab on Edge or Chrome is still refused with browser_capability_denied, because the extension drives the person's own browser through chrome.debugger and cannot open a page in it. The test drives the whole way -- the command reaches the extension, the extension refuses the method as outside its supported subset, and the refusal reaches the caller as browser_capability_denied. One browser fact the identity has to account for: a page that is still loading when the provider attaches to it is observed as a replaced page when its first load lands, so a newly opened page's generation may advance once, from 1 to 2. A page that finished loading before the provider attached stays at 1. The capability matrix records this as measured.
Issue #203. The runtime knew about a run's selection and about the browser's target list, but not about which pages the session had open as its tabs — so the client had to keep its own copy, and two copies of "what is open" disagree exactly when a tab is closed. The service now keeps the session's web tabs beside the run's selection and publishes them as a snapshot: each tab's id, its target identity, its title, its URL, and whether it is the run's selection. A snapshot rather than a delta — the list is small, and a snapshot is idempotent, so a client that reconnects reconciles against it instead of drifting. The snapshot travels on a new stream operation, watchBrowserWebTabs, modelled on watchBrowserFrames. Registering it is more than adding a name, and all eight sites are done: the capability manifest's browser.ready list, the operation-name constants, the operation spec, the contract type, the operation registry, the server's handler table, the port member and its binding, and the transport's request table. packages/webui/test/unit/browser-web-tabs-operation.test.ts is the executable form of that list — a site that loses the operation fails there rather than in a panel that quietly shows no tabs. The tab's id belongs to the client and the runtime's target identity is carried as the tab's binding, so a page that navigates keeps its target id and gains a generation while the web tab stays the same web tab. The provider forgets a page the browser destroyed, so a closed tab does not leave a page behind that a command could still be aimed at.
Issue #204. The frame stream took { surface, runId? } and the service resolved the run's selection to decide what to stream. That is what made the person and the Agent share one page, and it is also what stopped a person from looking at a second one. The stream is now keyed by the target: the client asks for the page it is showing, the service's frame subscribers are keyed by target, and a frame is delivered only to the tab that asked for it. One screencast per page, shared by that page's subscribers, because the provider holds one frame handler per page and two subscriptions for the same page would otherwise take each other's frames away. This is a change to an existing operation's request shape, not a second operation beside it. The name does not change; the request does. A client that still sends a run id is refused with an explicit error rather than answered with the run's selection, because a stream that silently answers with the wrong page is worse than one that says the request is out of date. The fence is unchanged and it is the whole point: a frame has to name the target the subscriber asked for, all four identity fields, and the page behind that target has to still be the one the frame is a picture of. A frame of another tab, or of a page that has been replaced, is dropped rather than put on screen — and the drop is counted, because a stream that quietly stops updating and a stream that is dropping frames look identical from the outside. The run's selection still decides what the Agent's commands act on. The two are different cells and this change does not move the other. The new real-browser case drives two pages of one real Chromium through the service and proves that neither stream receives the other's frames.
Issue #205. There was no per-session lifecycle signal to hang the release on, and a session is not only removed by deletion: archiving it removes one, and so does a transfer away. A release that covered only deletion would leave a page behind for every archived session, and a page in the isolated Chromium is a live process with a live target. The release is keyed on the session, and the edge it needed is the composition root's: the runtime has no per-session signal, and the composition root is the only place that holds both the harness port and the browser service, so it is the place that can wire the two together. The harness port's deleteSession and archiveSession now release the session, and the transfer's rollback reaches the same deleteSession, so all three ways a session goes away leave the same nothing behind. The release closes the page, stops its screencast, forgets the target in the registry and takes the run's selection with it. A target the registry still offers is a target a command could still be aimed at, and a screencast left running for a page that is gone is a resource the browser holds for nobody. A session that is still open keeps its pages: an explicit success of false means the session is still there, and releasing it would close a page out from under a running session. The new suite is the evidence for all three paths, and for the two things that must not happen: another session's pages are not closed because this one ended, and a subscriber receives no frame for a page that no longer exists.
… a dead end Issue #206. The tool adapter resolved the surface from the run's selection and refused with browser_target_unknown when the run had selected nothing. The refusal is right and stays for every action -- but it reached open_tab too, and open_tab is the action that would create the selection the gate wants. So open_tab is the one action a run with no selection may perform. It asks the service to open a page; the service observes the new target, makes it the run's selection, and publishes the new snapshot. The surface is a fact rather than a choice the run made: a run with a selection keeps the surface it selected on, and a run without one opens a page in the isolated Chromium, because the embedded panel is the isolated browser's view and the external browsers are driven in the person's own windows. The provider gains openTab as a first-class method, because open_tab is not aimed at a page -- it creates one -- and the page a run is about to open does not exist yet. A surface that cannot open a page has no such member, and the service refuses with browser_capability_denied, which is the extension bridge's case. The refusals keep their meaning. A surface whose switch is off is refused with browser_surface_not_authorized and no page is created. A surface that cannot open a tab is refused with browser_capability_denied. Every other action of a cold run is still refused with browser_target_unknown, so the refusal reaches no further than it did. The action is an explicit bootstrap and not a default: only the action that creates a selection creates one. A second open_tab moves the run's selection to the second page without losing the first. The new real-browser case drives the whole path: a cold run is refused everything, opens a tab, and then navigates the page it opened.
Issue #207. The tab's id was derived from its kind and its parts, so the id changed when the content did — and a web tab had no id at all that could tell two of them apart. The browser kind was one tab per session and directory, and the workspace's change set and a turn's review derived the same id, so opening one replaced the other. A tab is now a stable id plus a content descriptor, and the content carries the kind. The id is assigned by the client when the tab is opened and does not change when the content does: two web tabs in one session are two tabs, and a tab that changes what it shows is still the same tab. The rules that used to fall out of the id are content checks now, because the id says nothing about what the tab is showing. The same file opens one tab — per session and directory, as before. The workspace's change set and a turn's review are two tabs, told apart by the content's source. The files view, the canvas and the terminal are one tab per session and directory, and that is an explicit rule rather than a side effect of the id. The browser kind is gone from the enumeration and nothing mounts the browser tool tab: the panel's tabs are web tabs now. A web tab has no add-menu entry, because a web tab is opened by typing a URL or by the Agent. The stale-read hazard the mutable content brings is answered on both sides. The effects that fill the caches depend on the content, not on the id, so a content change re-runs them; and the file results are keyed by what the tab is showing rather than by which slot it is, so a tab that changed from one file to another reads the new one instead of keeping the old one's result.
Issue #208. The strip is the workspace's one row of tabs now: tool tabs (files, review, canvas, terminal) and the runtime's web tabs in one row. A web tab is opened by the runtime publishing it through `watchBrowserWebTabs`, and the strip turns each published tab into an `open-web-tab` command so the workspace's reducer holds the canonical list — the strip does not keep its own copy. Three things moved out of `WorkspacePanels.tsx` and into a dedicated `WebuiWorkspaceTabStrip.public.tsx` because the panel had grown over the size where "everything about the panel" still fits in one file: the per-tab close button, the active tab selection, and the new tab affordance. The new tab opens a tab with the new `launcher` content kind — the smallest shape that lets the strip open a tab as the new tab page rather than as a placeholder. The launcher UI itself (tiles and URL box) lives in the next programme step. A web tab shows the page's title and a default glyph; no favicon is fetched. The title follows the page: the reconciliation dispatches when the title, the url or the generation moved, and the reducer updates the content under the tab that already holds it — without moving the person off the tab they were looking at. A snapshot that changes nothing still produces no work. The surface switcher that used to sit at the top of `BrowserPanel` is gone. Edge and Chrome are reached in the person's own windows, and the only surface the panel ever shows is the isolated Chromium: `BrowserPanelHost` is now keyed by the web tab's target rather than by a switchable surface. The two published components keep their names so the Desktop fork's imports stay intact. The tool tabs keep the glyphs they always had — a file tab's file-type badge is a rendering of the content, and the content moved to the strip with the row. The strip owns the row, so the strip is what scrolls a focused, off-screen tab into view.
Issue #209. A newly opened tab shows a launcher: four tiles -- 变更 / 终端 / 画布 / 查看文件 -- and a URL box. Choosing either replaces that tab's content, because a tab is a stable id and a mutable content, and the slot the person opened is the slot that shows what they asked for. The tab keeps its id and is neither closed nor replaced. The URL box reaches the runtime through the browser business's port, which is the point of it. The Agent reaches open_tab through its browser tools and a person reaches it through the new tab page, and both now land on the same call: a new unary operation, openBrowserWebTab, whose handler calls WebuiBrowserPort.openWebTab -- the member the tool adapter already reached. The surface's switch, the provider gate, the session's web-tab record and the refusal codes are shared, so a switched-off surface refuses a typed url with the same browser_surface_not_authorized the Agent gets, before a provider is asked for a page. The request names a url and a run and no surface; a body with no run is the panel's own cell, so opening a page by hand does not move any run's selection. The answer and the runtime's own snapshot race, and both have to reach the tab that asked. So the launcher dispatches request-web-page before it calls the port, and the request rides on the launcher's content: an open-web-tab that names a tab binds the page there, and an open-web-tab that names none -- the snapshot's shape -- binds it to a launcher that is waiting for a page. Either order, one page, one tab. Neither activates the tab, so a person who looked elsewhere while the call was in flight keeps looking at what they were looking at. The add menu is gone and its entries are the launcher's tiles. The rows that described them stayed and were renamed -- addMenu becomes tile, webuiWorkspaceAddMenu becomes webuiWorkspaceLauncherTiles -- because a menu that no longer exists is not a name anything should keep, and the menu's one rule was the one worth keeping: the launcher's list is the registry's rows that carry a tile, never a second enumeration. The tiles use the icons the menu used. addMenuOpen and toggle-add-menu are gone with the menu, rather than left driving nothing. open-new-tab opens a fresh new tab page every time. The singleton rule was right while a launcher was a placeholder, and wrong now: the page a person typed a url into is a web page with no url box left, so pressing "+" again is how they ask for another one, without losing the first. A bare host is given a scheme before the browser is told, because a person types a host as often as a scheme and a host is not something the browser can open. A refusal is the runtime's own message on the launcher, and the launcher keeps its box: a page that cannot be opened is not a closed page. The launcher lives beside the strip that owns the row rather than in a new file: the ADR gives it a home, and a component that belongs to an existing one costs no new source path, no frozen-fixture entry and no suite-group entry. The next step is a web tab's own toolbar -- back, forward, reload and a read-only URL -- and the bootstrap that gives a cold run the page a person opened. Neither is in this shape.
A web tab was a picture and nothing else: there was no way to go back, to go
forward, or to re-read the page, and the only way to reach another url was to
open another tab. The toolbar is back, forward, reload, and the url shown as
text — the three commands a tab does to the page it holds, and no address box,
because the only place a person types a url is the new tab page.
The toolbar is the browser business's chrome, rendered by the published host the
strip already mounts. The strip owns the row of tabs; a workspace component
drawing a foreign business's page chrome would invert the dependency rule, and
ADR 0022's component table had already given the host this job ("binds a tab to
a target, shows the picture and the toolbar"). So the strip hands over the tab's
target and the url the runtime published with it, and the host drives the port.
Both are values rather than authority: the host, not the strip, decides what is
sent.
The three presses go through the existing `sendBrowserCommand` operation — no
second operation, no second route. What the request gained is an optional
target, because a web tab holds exactly one page and the person pressing back
means the page in front of them: a request without a target still resolves the
surface's selection, which is a run's page or the panel's own cell and is not
necessarily the page the tab is showing. The handler routes a named target to
the service's new `sendCommandToTarget`, which makes the same fences in the
same order — the surface's switch, the command gate, then the generation — and
deliberately moves no selection, since a tab's toolbar opens nothing. The Agent's
tools are untouched: a request with no target means exactly what it meant.
A refusal is not papered over. A bridge that is switched off still answers
`browser_capability_denied`, a page that moved still answers
`browser_target_stale`, and the page is left alone.
Measured against a real isolated Chromium: `back` lands on the page it came
from, `forward` on the one it left, `reload` re-reads the page it is on, the
generation advances with each move so the pre-move reference is refused while
the tab's current one still works, and a `back` with nothing behind it is
refused with the provider's own code.
The panel had one selection and two writers. The Agent wrote it through `open_tab`, and a person wrote it by clicking a row in the panel's target list — and the panel then bound its pointer, key and text gestures to it, so a person typing in their own tab typed into the page the Agent was working on. One cell with two writers is not a selection at all; it is whichever writer went last. ADR 0022 already decided the shape. There are two cells, one writer each: the run's selection is the runtime's, and the Agent writes it; the tab the person is looking at is the client's, and the person writes it. This change splits them where the code could not tell them apart, and names them where the documents had to explain them. The client's cell is `WebuiBrowserPanelState.viewedPage`, written by the strip through the host's `page-viewed` and read by everything that shows or forwards input: the picture, the alt text, the frame stream and the gesture. `prepareWebuiBrowserInputCommand` now takes that page as its own argument, so there is no field of the run's selection left in its signature for a gesture to be aimed at — the binding cannot drift back without someone adding the field again. The runtime's cell is `runSelection`, mirrored from the mark the runtime puts on the target list. The panel reports it and cannot change it: the target list is a report rather than a control, because a person choosing a tab is not a way to move the Agent. The one write that crosses cells is the bootstrap, and the runtime makes it. `openWebTab` now says who opened the page — `WebuiBrowserTabOpener` is `"agent"` or `"person"`, and the `openBrowserWebTab` handler is the person's side — and a person-opened page joins the run's selection only when the run had none at the moment the page appeared. That decision cannot live on the client: the client cannot see a run's selection at all. So the workspace names its session in the request and applies nothing itself, and an Agent-opened page is unconditional, because a run writing its own cell is not a bootstrap and must not become one. The strip's web-tab subscription follows the same run, so a session's pages are recorded in one cell whether the Agent or the person opened them. A real isolated Chromium shows the two apart: the Agent drove one page while the person typed in another and neither page received the other's input; on the page the bootstrap handed a cold run, both were delivered; and an input recorded before a page moved was refused with `browser_target_stale` rather than dropped in silence.
Issue #212. The acceptance record for this feature is written **from** the records the suites write, so this adds the suite that writes them: a real isolated Chromium, the Agent's tool opening a real tab, the strip gaining it, the picture updating, and the person taking over. Fourteen cases, one per row of the acceptance matrix, each observed through the shipped paths — the Agent's tool path through the browser tool adapter, the person's launcher path through the server's own handler, the input path through the client's own binding, and the provider itself. The records land in this programme's own completion directory, so a case from one programme can never be read as a case from the other: the acceptance helper is now plan-scoped, and the previous programme's suites keep writing where they always have. Writing the acceptance found a race. A page that is still loading is observed as a replaced page when its first load lands, and that observation can arrive before the answer that opened it does — so a selection written from the answer alone was born stale, and the Agent's next action was refused against a page it had just opened. The service now writes the registry's generation, which is the current one, and the selection follows the page from there. The documentation is caught up with the code. The tab model's rules become `implemented`, each with the file and the line that carries it; the rules that are not the tab model's — a caller's obligation, a concurrency rule the tree does not enforce — stay `required` and say so. Round 11's four findings are closed, each with the evidence that closed it. The known gaps and the open questions name this programme's state honestly, the ADR says it is implemented, and the glossary names the client's cell.
An independent review of the tab model (codex, gpt-6-luna) found five things, and the gate found a sixth. All six are fixed here. **Closing a web tab left its page running.** The strip's close button dispatched the client command and nothing else: the runtime kept publishing the tab and the isolated Chromium kept the page alive, and the next snapshot could make the supposedly closed tab reappear. There is now a `closeBrowserWebTab` operation beside `openBrowserWebTab`, and the button goes through it. The page is closed in the browser **first** and the tab leaves the strip after: the runtime stops publishing the tab when its page is closed, and a tab that left the strip while the runtime still published it would be re-added by the next snapshot. **The person’s launcher could be refused because the Agent was working in Edge.** The launcher sent the run’s id, and the runtime chose the run’s *selected* surface — so a url typed in the panel went to the bridge, which cannot open a page. The panel is the isolated browser’s view, so the person’s page is always opened in the isolated Chromium, and the run keeps the page it was working on. **The bootstrap could race an Agent selection.** The run’s selection is now read before the page is created, and it is read across every surface: the Agent acts on one page, so the bootstrap is about the run and not about a surface. **The acceptance record overstated what it measured.** The case now reads the runtime’s own published stream rather than a fabricated snapshot, and it says what it did: the runtime publishes the tab, the strip’s reconciliation turns it into a panel command, and the reducer makes it the active tab. The mounted strip is covered by the unit suites. **The authorization trace’s source pointers were stale.** They name the fence that is there now. And the gate found the sixth: the file results were read by the tab id while the read was filed by the content key, so a file never appeared. The render reads the key the read writes, and the effect that reads a file follows the content key rather than the content object — a content replaced by an equal one is the same file, and re-reading it superseded the read that was already in flight.
The plan says the slice is delivered, names the board that carried it, and says that the plan is tracked — the previous programme's plan was left out of the repository, and an implementer working in a worktree could not see it. The port-slices test names the browser slice's members, and the close operation is one of them now.
The programme was reviewed with the local OpenAI Codex through paseo (`codex/gpt-6-luna`). The review found five things and the WebUI gate found a sixth: closing a web tab left its page running, the person's launcher could be refused because the Agent was working in Edge, the bootstrap could race an Agent selection, the acceptance record overstated what it measured about the client strip, the authorization trace's source pointers were stale, and the file results never appeared. The record is here so the review is part of the programme's evidence rather than a claim in a commit message: what the reviewer was asked for, what it confirmed, each finding and what closed it, and what it did not check.
The review record is a new file in the published projection, and the inventory is what says so. Without it `check:source` fails — one of the plan's own gates.
83d8052 to
5d15467
Compare
The plan's §7 names six gates and §9 makes them the completion definition. This is that record — the commands, the results, the environment and the tree — so the claim can be checked without running anything again. It also says plainly how often the gate ran: three times, each after a whole block of changes, and the first two runs each found a real defect. The focused suites ran per issue; the full gate did not.
Two real defects, both found by the gate and both intermittent. **The listing compared a selection it had read before the listing.** The service read the run's selection once, before asking the provider for its targets, and compared that snapshot with each target it came back with. A page that is still loading is observed as a replaced page, and that observation moves the generation while the listing is in flight — so the snapshot was a generation behind, the comparison failed, and the tab was reported as not selected. The selection is now read per target, after the listing, and it follows the page it names: the selection names a page, and a page that navigated is still the same page. **The registry could end up behind the provider.** The announcement that a page was replaced arrives at the service, and the registry raises the generation — but a page that is still loading is observed as a replaced page while it is being attached to, and the announcement can arrive before the service has observed the target at all. The raise was then lost, and the registry kept a generation the provider had already moved past. The registry now adopts a provider-reported generation that is **ahead**: the provider's accounting is the authority on which page is there now. A sighting that reports the same generation, or an older one, still changes nothing — a repeated sighting must not invalidate a reference. **The cleanup raced the browser.** The provider removes its profile directory right after killing the browser, and the browser may still be writing it: the removal failed with ENOTEMPTY and failed the suite's file, with every test in it passing. The removal is now retried. The unit suite was run four times after the fix: 208 files and 3230 tests, every time.
The gate had been red twice after the plan was delivered, both times for a real defect in the service: a selection compared against a generation that had moved while the listing was in flight, and a profile directory removed while the browser was still writing it. Both are fixed, and the record says so rather than claiming a gate that had not been run since.
The fork's CI runs the repository's whole verify profile, which is wider than the plan's own gates. Its history on this branch and on the base branch is now recorded, including the two jobs the base branch already failed before this programme started.
The claim is checkable against a commit rather than against a tree: the gates were run at 4e7efd4, and the only commits since are this record and the inventory line it adds. It also records the repository's capability suite, which the CI had called red: it passes here, so that failure belongs to the macOS runner and not to this change.
a26311f to
acbf1b4
Compare
…ommit The CI's ubuntu job is red on six external-bridge cases that need a real external browser with the extension; the same cases pass here in five seconds, and the extension and the bridge worker are not touched by this slice. The webui unit suite and the installed-package check both pass in CI.
The verify matrix runs on pull requests, so the fork's webui branch has one Source verification run in its history, at an older commit, and it failed the macOS and ubuntu jobs. The branch also carries a second workflow that has failed on every recent commit. Neither is this slice's.
The external-bridge spec's guard cannot fire for the chromium channel, because the installed-browser probe returns true for it unconditionally. So the case runs on a runner that cannot drive a real browser with the extension and fails as a timeout, where the spec's own comment says it should say so instead. Tightening that probe is a change to the previous programme's spec, in an area this slice does not cover. It is recorded, not made.
…at it did not The record carried a wrong claim about the CI's ubuntu failure: it said the runner "cannot drive a real browser with the extension". The same job's sibling spec passes in both channels -- the handshake completes in 785 ms in chromium and in 22.6 s in msedge -- so the runner does drive a real browser with the built extension, and the extension does pair with the host. What is true is narrower: the external spec's operations against the live tab never come back. It also carried the wrong verdict for the macOS job. At the tip, the matrix cancelled it when the ubuntu job failed, so it has no verdict at all -- not a pass. It passed at acbf1b4, on the same code. And it did not say where the redness comes from. The same six cases failed the same way on a18c916, the browser-use programme's own head commit, merged as PR #195 and an ancestor of this branch's base: 138 passed, 6 failed, at test:webui-browser. This slice changes neither the extension, nor the bridge server, nor that spec, so the redness is inherited rather than introduced. The record now says the gate ran five times rather than once -- with what each run found -- and separates those runs from the targeted re-runs of a single suite. It names the commit whose subject covers only part of what the commit changes, and why that subject was not rewritten. The review record now carries the invocation itself: the paseo agent, the model, the thinking option, the Codex session, the tokens, and the prompt verbatim, so the review is not only attested by the commit that fixed it.
The six external-bridge cases fail on the CI runner with the test's own 120-second timeout and nothing else: the report says the test timed out, and not which step stopped. That is because several of the harness's waits on the extension have no deadline of their own -- a service worker Chrome has reclaimed does not answer, and a message nobody replies to does not settle -- and a wait that hangs there is indistinguishable from a slow case. Every wait in the harness that can outlive its case is now bounded, and each bound names the step it was waiting for. A case that cannot connect now says "the extension did not answer the reconnect message" instead of running into Playwright's timeout. This does not make a failing case pass, and it does not skip one: a bound that expires fails the case with a diagnosis. It is here so that the next run of these cases on the CI runner says where they stop.
|
A correction to what this pull request said about the gate, and what the CI says. The body of this PR said "The WebUI gates pass". That was true of the local gate at the time and it is still true of it, but it did not say where, and the CI disagrees at one step. The body now says both: the §7 gates are green at this tip, and in CI Those six are inherited, not introduced here. They failed the same way on $ gh run view 39538891164 -R mcode-webui/minimax-code --log-failed \
| grep -oE "✘ +[0-9]+ packages/webui/test/browser/[^ ]+|[0-9]+ failed|Verification failed at: .*"
✘ 20 packages/webui/test/browser/browser-bridge-real-external.spec.ts:82:5
…
6 failed
Verification failed at: test:webui-browserand this branch changes neither the extension, nor the bridge server, nor that spec. The full correction, with the commands, is on the board's specification issue (#199) and in The harness's waits on the extension had no deadlines, so a case that hung there reported only "the test timed out" — with nothing about which step stopped. They are bounded now, and each bound names its step ( |
… server The six external-bridge cases fail on the CI runner with the test's own 120-second timeout and no other information. With the waits bounded, the failure names itself: the case's work is finished, and the harness's http server does not close. The browser is still running while the harness closes its server, so the browser may still be holding a connection -- the extension's socket, or the keep-alive connection the test page was fetched over -- and a server that waits for a client to go away first cannot close while that client is still running. The extension makes it worse: it reconnects to a host that is going away. So the browser stops first, and the harness cuts the connections it still holds instead of waiting for them. This is test-side only, and it changes no assertion: the case's work is unchanged, and a cleanup that cannot finish now says which step stopped.
… stop The bounded waits named the step: the cases' work is finished, and the harness's http server does not close. The browser is still running while the harness closes its server, so it may still be holding a connection -- the extension's socket, or the keep-alive connection the test page was fetched over -- and a server that waits for a client to go away first cannot close while that client is still running. The record now carries that, the two test-side commits that answer it, and the CI's verdicts as they stand: macOS passes (it had been cancelled with the matrix at the previous tip, which is not a verdict), and the ubuntu job is red on the six cases that were already red on the base branch's own head. The whole of §7 was run again at the tip that carries this record, after the two commits that follow the plan's implementation: types, 208 files and 3230 unit tests, dependency direction, build, build boundary, the source projection (5407 paths) and 146 browser cases in 2.0m. The acceptance records the suites write are updated with that run.
The cleanup fix turned the six external-bridge cases green on the runner that had been failing them: the CI run at 54db73a has `verify (ubuntu-latest)` passing -- test:webui 208 files, test:webui-browser 146 passed in 2.8m, the source projection 5407 files, the dependency and build-boundary checks, the installed package -- and the windows job with it. That is the first green ubuntu run since the browser-use programme landed those cases, and it makes the fork's CI red for a reason this slice owns rather than one it inherited. The macOS job of that run was cancelled while it was still queued, so it carries no verdict; the run at 9d945e5 passed macOS in 14m51s. The record says both, and says which of them is a verdict and which is the runner queue.
…t is The document that carries round 11 still ended on the sentence "since round 11 it is not even reachable: the Agent cannot acquire a selection, the isolated provider drives one page, and the image stream is per run rather than per target". Those three things are T1, T2 and T3 -- the findings this programme closed -- so the sentence describes the tree as it was before the tab model, in the present tense. It now says what is true: the path is reachable, the tab model's own run exercises it end to end against a real Chrome, and what that run does not establish is the same path on the external bridge's surfaces. The gate record says the CI's verdicts as they stand: the run at 3d3b784 has all three platforms passing and the workflow's own "Require every planned check" gate passing with them.
|
The CI is green, and the six external-bridge cases that were red since the previous programme are fixed. The CI run at They were diagnosed. The cases hung with no diagnosis, because the test harness had waits with no deadline of their own — a service worker Chrome has reclaimed does not answer, and a message nobody replies to does not settle. Every such wait is now bounded, and each bound names the step it was waiting for ( It said where it stopped: The cases' work was finished — the browser started, the extension paired, the handshake completed, the DevTools command ran against the live tab. What hung was the cleanup: the browser was still running while the harness closed its server, so it may still have been holding a connection, and the extension reconnects to a host that is going away. A server that waits for a client to go away first cannot close while that client is still running. It is fixed ( The whole of §7 was run again locally at the tip that carries the record: types, 208 files and 3230 unit tests, dependency direction, build, build boundary, the source projection (5407 paths) and 146 browser cases in 2.0m. The record is |
The CI run at 4a0a5d9 failed the ubuntu job on a suite file whose every test passed: FAIL packages/webui/test/integration/browser-use-real-input.test.ts Error: ENOTEMPTY: directory not empty, rmdir '/tmp/webui-browser-use-e2e-*/browser' The browser is killed and its profile directory is removed straight away, and the browser may still be writing its last files. The provider's own removal already retries for exactly this reason (`maxRetries: 5`, `retryDelay: 100`); the tests that remove a browser's profile did not, so the race surfaced there instead. Every removal of a browser's profile directory now retries the same way. A profile is a directory the browser owned, and the browser is gone: a removal that races with its last writes is retried rather than reported as a failure.
…ner flake The run at 4a0a5d9 failed the ubuntu job on a suite file whose every test passed: the browser is killed and its profile directory is removed straight away, and the browser may still be writing its last files. The provider's own removal has retried for that reason since 782c137; the tests that remove a browser's profile did not, so the race surfaced there instead. Every such removal retries now (9ac6762). The same run failed the windows job on the runner's PowerShell timing out while it checked the checkout volume -- a repository-level gate this slice does not touch, which the runs on either side of it passed. The CI run at 9ac6762 has all three platforms passing, and the workflow's own "Require every planned check" gate passing with them.
|
The final state of the CI, stated precisely.
The head differs from the two green runs by three generated record files only — the acceptance records the suites write and the gate record itself: $ git diff --name-only 9ac67622..b495a4dc
.plan/browser-use/complete/isolated-chromium.acceptance.json
.plan/workspace-browser-tab/complete/gate-record.md
.plan/workspace-browser-tab/complete/workspace-browser-tab.acceptance.jsonNo source file, no test and no runtime behaviour differs from the commit whose macOS job passed. The macOS job at this head is waiting for a macOS runner, and it has been cancelled twice without ever starting; that is the runner queue, not a verdict, and the record says so in the same words. |
|
The CI is green at this pull request's head. The
The six external-bridge cases that had been red on that runner since the browser-use programme introduced them are green now: the harness names the step a failure stops at ( |
The workspace panel becomes a browser tab strip, per ADR 0022.
A tab is a stable id plus a mutable content, and the content carries the kind. The id is assigned by the client when the tab is opened and does not change when the content does, so two web tabs in one session are two tabs, and a tab that changes what it shows is still the same tab.
The runtime
event.sessionId— never byevent.params.sessionId, which is the number1for frames from both pages.watchBrowserWebTabs. A snapshot rather than a delta: it is idempotent, so a client that reconnects reconciles against it instead of drifting.open_tabanswers with the identity of the page it created, and it is the one action a run with no selection may perform: it is the action that would create the selection the gate wants.The client
browserkind — and with the surface switcher, because the panel is the isolated browser's view.Evidence
The capability matrix (
packages/webui/docs/browser-use/01-capability-matrix.md) is written from measurements, and every rule ADR 0022 pins isimplementedwith the file and the line that carries it. The acceptance record carries a real-browser row for the Agent's path, written from the records the suite writes.The gate. The plan's §7 gates are green at the tip this pull request carries: typecheck, 208 files and 3230 unit tests, dependency direction, build, build boundary, the source projection (5407 published paths), 146 real-browser tests against real Chrome 152, and the installed package.
In CI the same gates are green: the run at
54db73a3hasverify (ubuntu-latest)andverify (windows-latest)passing —test:webui208 files,test:webui-browser146 passed, the source projection 5407 paths, the dependency and build-boundary checks, and the installed package.The browser suite had been red on that runner since the previous programme landed its external-bridge cases (
a18c916b, PR #195) — they hung with no diagnosis, because the test harness had waits with no deadline of their own. The harness now bounds every such wait and names its step (9d945e54), and the cleanup stops the browser before the server it was holding open (a4f8f7c6). The record is.plan/workspace-browser-tab/complete/gate-record.md.Closes #199, closes #200, closes #201, closes #202, closes #203, closes #204, closes #205, closes #206, closes #207, closes #208, closes #209, closes #210, closes #211, closes #212